home *** CD-ROM | disk | FTP | other *** search
- <?php
- /* vim: set expandtab tabstop=4 shiftwidth=4: */
- // +----------------------------------------------------------------------+
- // | Copyright (c) 2002-2003 Brent Cook |
- // +----------------------------------------------------------------------+
- // | This library is free software; you can redistribute it and/or |
- // | modify it under the terms of the GNU Lesser General Public |
- // | License as published by the Free Software Foundation; either |
- // | version 2.1 of the License, or (at your option) any later version. |
- // | |
- // | This library is distributed in the hope that it will be useful, |
- // | but WITHOUT ANY WARRANTY; without even the implied warranty of |
- // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
- // | Lesser General Public License for more details. |
- // | |
- // | You should have received a copy of the GNU Lesser General Public |
- // | License along with this library; if not, write to the Free Software |
- // | Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA|
- // +----------------------------------------------------------------------+
- //
- // $Id: File.php,v 1.12 2003/01/27 04:31:42 busterb Exp $
-
- require_once 'DBA.php';
-
- // {{{ constants
- /**
- * Location in the index file for a block location
- */
- define('DBA_SIMPLE_LOC',0);
-
- /**
- * Location in the index file for a block size
- */
- define('DBA_SIMPLE_SIZE',1);
-
- /**
- * Location in the index file for a block value size
- */
- define('DBA_SIMPLE_VSIZE',2);
-
- /**
- * Location in the index file for a block key
- */
- define('DBA_SIMPLE_KEY',3);
- // }}}
-
- /**
- * DBA_Driver_File provides a simple, file-based implementation of a
- * DBM-style database. It uses two files, and index and a data file to
- * manage key/value pairs. These two files use the suffixes '.dat' and
- * '.idx'. When a database is opened, only the index file is read. The
- * index file contains pointers to locations within the data file, which
- * are used to retrieve values.
- *
- * The class uses a concept of blocks for data storage. When the first value
- * is inserted, a new block is created by appending to the data file. If that
- * value is deleted, it remains in the data file, but is marked as empty in
- * the index file. A list of available blocks is kept, so when a new value
- * is inserted, its size is compared to the list of available blocks. If one
- * is of sufficient size, it is reused and marked as used in the index file.
- * Blocks can be of any length.
- *
- * In updating the index, lines are simply appended to the file after each
- * operation. So, the index file might have the same block listed multiple time
- * , just in different states. When the database is closed, it rewrites the
- * index file, removing and duplicate entries for a single block. The index
- * reader only uses the last entry for a block from the index file, so if close
- * is not called for some reason, the index file is still in a valid state.
- *
- * The optimize function merely removes duplicated index entries by rewriting
- * the file, the same as close.
- * The sync function calls fflush on the data and index files.
- *
- * @author Brent Cook
- * @version 1.0
- * @access public
- * @package DBA
- */
- class DBA_Driver_File extends DBA
- {
- // {{{ instance variables
- /**
- * Name of the database
- * @access private
- */
- var $_dbName;
-
- /**
- * Handle to data file
- * @access private
- */
- var $_datFP;
-
- /**
- * Handle to index file
- * @access private
- */
- var $_idxFP;
-
- /**
- * Indicates the current ability for read/write operations
- * @access private
- */
- var $_writable;
-
- /**
- * Indicates the current ability for read operations
- * @access private
- */
- var $_readable;
-
- /**
- * Determines if the driver should use an index file
- * @access private
- */
- var $_indexed;
- // }}}
-
- // {{{ DBA_Driver_File($indexed = true)
- /* Constructor
- *
- * @access public
- * @param string $driver dba driver to use
- */
- function DBA_Driver_File($indexed = true)
- {
- // call the base constructor
- $this->DBA();
- $this->_indexed = true;
- }
- // }}}
-
- // {{{ open($dbName='', $mode='r', $persistent = false)
- /**
- * Opens a database.
- *
- * @access public
- * @param string $dbName The name of a database
- * @param string $mode The mode in which to open a database.
- * 'r' opens read-only.
- * 'w' opens read-write.
- * 'n' creates a new database and opens read-write.
- * 'c' creates a new database if the database does not
- * exist and opens read-write.
- * @param boolean $persistent Determines whether to open the database
- * peristently. Not supported here.
- * @return object PEAR_Error on failure
- */
- function open($dbName='', $mode='r', $persistent = false)
- {
- if ($persistent) {
- return $this->raiseError(DBA_ERROR_UNSUP_PERSISTENCE);
- }
-
- if ($dbName == '') {
- return $this->raiseError(DBA_ERROR_NO_DBNAME);
- } else {
- $this->_dbName = $dbName;
- $dat_name = $dbName.'.dat';
- $idx_name = $dbName.'.idx';
- }
-
- switch ($mode) {
- case 'r':
- // open for reading
- $file_mode = 'rb';
- $this->_writable = false;
- $this->_readable = true;
- break;
- case 'n':
- // create a new database
- $file_mode = 'w+b';
- $this->_writable = true;
- $this->_readable = true;
- break;
- case 'c':
- // should we create a new database?
- if (!DBA_Driver_File::db_exists($dbName)) {
- $file_mode = 'w+b';
- $this->_writable = true;
- $this->_readable = true;
- break;
- } // otherwise, we just open for writing
- case 'w':
- $file_mode = 'r+b';
- $this->_writable = true;
- $this->_readable = true;
- break;
- default:
- return $this->raiseError(DBA_ERROR_INVALID_MODE, NULL, NULL,
- 'filemode: '.$mode);
- }
-
- // open the index file
- $this->_idxFP = @fopen($idx_name, $file_mode);
- if ($this->_idxFP === false) {
- $this->_writable = false;
- $this->_readable = false;
- return $this->raiseError(DBA_ERROR_CANNOT_OPEN, NULL, NULL,
- 'could not open idx file '.$idx_name);
- }
-
- // open the data file
- $this->_datFP = @fopen($dat_name, $file_mode);
- if ($this->_datFP === false) {
- fclose ($this->_idxFP);
- $this->_writable = false;
- $this->_readable = false;
- return $this->raiseError(DBA_ERROR_CANNOT_OPEN, NULL, NULL,
- 'could not open data file '.$dat_name);
- }
-
- // get a shared lock if read-only, otherwise get an exclusive lock
- if ($file_mode == 'r') {
- flock ($this -> _idxFP, LOCK_SH);
- flock ($this -> _datFP, LOCK_SH);
- } else {
- flock ($this -> _idxFP, LOCK_EX);
- flock ($this -> _datFP, LOCK_EX);
- }
-
- // we are writing to a new file, so we do not need to read anything
- if ($file_mode != 'w+') {
- // parse the index file
- $this->_readIdx();
- }
- }
- // }}}
-
- // {{{ close()
- /**
- * Closes an open database.
- *
- * @access public
- * @return object PEAR_Error on failure
- */
- function close()
- {
- if ($this->isOpen())
- {
- if ($this->isWritable())
- {
- $this->_writeIdx();
- }
- $this->_readable = false;
- $this->_writable = false;
- fclose($this->_idxFP);
- fclose($this->_datFP);
- } else {
- return $this->raiseError(DBA_ERROR_NOT_OPEN);
- }
- }
- // }}}
-
- // {{{ reopen($mode)
- /**
- * Reopens an already open database in read-only or write mode.
- * If the database is already in the requested mode, then this function
- * does nothing.
- *
- * @access public
- * @param string $mode 'r' for read-only, 'w' for read/write
- * @return object PEAR_Error on failure
- */
- function reopen($mode)
- {
- if ($this->isOpen()) {
- if (($mode == 'r') && $this->isWritable()) {
- // Reopening as read-only
- $this->close();
- return $this->open($this->_dbName, 'r');
- } else {
- if (($mode == 'w') && (!$this -> _writable))
- {
- // Reopening as read-write
- $this->close();
- return $this->open($this->_dbName, 'w');
- }
- }
- } else {
- return $this->raiseError(DBA_ERROR_NOT_OPEN);
- }
- }
- // }}}
-
- // {{{ _DBA_Driver_File()
- /**
- * PEAR emulated destructor calls close on PHP shutdown
- * @access private
- */
- function _DBA_Driver_File()
- {
- $this->close();
- }
- // }}}
-
- // {{{ getName()
- /**
- * Returns the name of the opened database. Assumes database is open
- * @returns string the name of the opened database
- */
- function getName()
- {
- return $this->_dbName;
- }
- // }}}
-
- // {{{ isOpen()
- /**
- * Returns the current open status for the database
- *
- * @access public
- * @return boolean true if the database is open, false if it is closed
- */
- function isOpen()
- {
- return($this->_readable || $this->_writable);
- }
- // }}}
-
- // {{{ isReadable()
- /**
- * Returns the current read status for the database
- *
- * @access public
- * @return boolean true if the database is readable, false if it is not
- */
- function isReadable()
- {
- return $this->_readable;
- }
- // }}}
-
- // {{{ isWritable()
- /**
- * Returns the current write status for the database
- *
- * @access public
- * @return boolean true if the database is writable, false if it is not
- */
- function isWritable()
- {
- return $this->_writable;
- }
- // }}}
-
- // {{{ remove($key)
- /**
- * Removes the value at location $key
- *
- * @access public
- * @param string $key key to remove
- * @return object PEAR_Error on failure
- */
- function remove($key)
- {
- if ($this->isWritable()) {
- if (isset($this->_usedBlocks[$key])) {
- $this->_freeUsedBlock($key);
- } else {
- return $this->raiseError(DBA_ERROR_NOT_FOUND, NULL, NULL, 'key: '.$key);
- }
- } else {
- return $this->raiseError(DBA_ERROR_NOT_WRITEABLE);
- }
- }
- // }}}
-
- // {{{ &fetch($key)
- /**
- * Returns the value that is stored at $key.
- *
- * @access public
- * @param string $key key to examine
- * @return mixed the requested value on success, false on failure
- */
- function &fetch($key)
- {
- if ($this->isReadable()) {
- if (!isset($this->_usedBlocks[$key])) {
- return $this->raiseError(DBA_ERROR_NOT_FOUND, NULL, NULL, 'key: '.$key);
- } else {
- fseek($this->_datFP, $this->_usedBlocks[$key][DBA_SIMPLE_LOC]);
- return fread($this->_datFP,$this->_usedBlocks[$key][DBA_SIMPLE_VSIZE]);
- }
- } else {
- return $this->raiseError(DBA_ERROR_NOT_READABLE);
- }
- }
- // }}}
-
- // {{{ firstkey()
- /**
- * Returns the first key in the database
- *
- * @access public
- * @return mixed string on success, false on failure
- */
- function firstkey()
- {
- if ($this->isReadable() && ($this->size() > 0)) {
- reset($this->_usedBlocks);
- return key($this->_usedBlocks);
- } else {
- return false;
- }
- }
- // }}}
-
- // {{{ nextkey()
- /**
- * Returns the next key in the database, false if there is a problem
- *
- * @access public
- * @return mixed string on success, false on failure
- */
- function nextkey()
- {
- if ($this->isReadable() && ($this->size() > 0)
- && next($this->_usedBlocks)) {
- return key($this->_usedBlocks);
- } else {
- return false;
- }
- }
- // }}}
-
-
- // {{{ getkeys()
- /**
- * Returns all keys in the database
- *
- * @access public
- * @return mixed array
- */
- function getkeys()
- {
- if ($this->isReadable() && ($this->size() > 0)) {
- return array_keys($this->_usedBlocks);
- } else {
- return array();
- }
- }
- // }}}
-
- // {{{ size()
- /**
- * Calculates the size of the database in number of keys
- *
- * @access public
- * @return int number of keys in the database
- */
- function size()
- {
- if (is_array($this->_usedBlocks)) {
- return sizeof($this->_usedBlocks);
- } else {
- return 0;
- }
- }
- // }}}
-
- // {{{ insert($key, $value)
- /**
- * Inserts a new value at $key. Will not overwrite if the key/value pair
- * already exist
- *
- * @access public
- * @param string $key key to insert
- * @param string $value value to store
- * @return object PEAR_Error on failure
- */
- function insert($key, $value)
- {
- if ($this->exists($key)) {
- return $this->raiseError(DBA_ERROR_ALREADY_EXISTS, NULL, NULL,
- 'key: '.$key);
- } else {
- return $this->replace($key, $value);
- }
- }
- // }}}
-
- // {{{ replace($key, $value)
- /**
- * Inserts a new value at key. If the key/value pair
- * already exist, overwrites the value
- *
- * @access public
- * @param $key string the key to insert
- * @param $val string the value to store
- * @return object PEAR_Error on failure
- */
- function replace($key, $value)
- {
- // is the database in a usable state?
- if ($this->isWritable()) {
- // get how much space we need
- $vsize = strlen($value);
-
- if (!isset($this->_usedBlocks[$key])) {
- // the value is new
- $this->_writeNewBlock($key, $value, $vsize);
-
- } else {
- // the value is not new
- $size = $this->_usedBlocks[$key][DBA_SIMPLE_SIZE];
-
- // is the value smaller or equal in size to its block size
- if ($size >= $vsize) {
- // move to the block's location in the data file
- $loc = $this->_usedBlocks[$key][DBA_SIMPLE_LOC];
- fseek($this->_datFP, $loc);
-
- // write to the data file
- fwrite($this->_datFP, str_pad($value, $size), $size);
-
- // update internal indecies
- $this->_usedBlocks[$key][DBA_SIMPLE_VSIZE] = $vsize;
- $this->_writeIdxEntry($loc, $size, $vsize, $key);
-
- // the value is larger than its allocated space
- } else {
- // free this value's allocated block
- $this->_freeUsedBlock($key);
-
- $this->_writeNewBlock($key, $value, $vsize);
- }
- }
- } else {
- return $this->raiseError(DBA_ERROR_NOT_WRITEABLE);
- }
- }
- // }}}
-
- // {{{ _writeNewBlock($key, $value, $vsize)
- /**
- * Allocates a new block of at least $vsize and writes $key=>$val
- * to the database
- *
- * @access private
- * @param string $key
- * @param string $value
- * @param int $vsize
- */
- function _writeNewBlock($key, $value, $vsize)
- {
- // is there is a sufficiently sized block free ?
- $loc = $this->_getFreeBlock($vsize);
- if ($loc !== false) {
- // update free block list
- $size = $this->_freeBlocks[$loc];
- unset($this->_freeBlocks[$loc]);
-
- // move to the block's location in the data file
- fseek($this->_datFP, $loc, SEEK_SET);
-
- // write to the data file
- fwrite($this->_datFP, str_pad($value,$size), $size);
-
- $this->_usedBlocks[$key] = array($loc, $size, $vsize);
- $this->_writeIdxEntry($loc, $size, $vsize, $key);
-
- // there is not a sufficiently sized block free
- } else {
- // move to the end of the data file
- fseek($this ->_datFP, 0, SEEK_END);
- $loc = ftell($this->_datFP);
-
- // write to the data file
- $size = $vsize + ceil($vsize / 20); // make size 5% larger
-
- // add a useless "\n" to new values. This makes the data file
- // readable in any text editor. Useful when things go wrong :P
- fwrite($this->_datFP, str_pad($value, $size)."\n", $size+1);
-
- // update internal block lists
- $this->_usedBlocks[$key] = array($loc, $size, $vsize);
- $this->_writeIdxEntry($loc, $size, $vsize, $key);
- }
- }
- // }}}
-
- // {{{ _getFreeBlock($reqsize)
- /**
- * Returns a block location from the free list
- *
- * @access private
- * @param int $reqsize Requested size
- * @returns mixed location of free block, false if there are no free blocks
- */
- function _getFreeBlock($reqsize)
- {
- // check if we have any blocks to choose from
- if (is_array($this->_freeBlocks)) {
- // iterate through the blocks in blockIndex to find
- // a free block
- foreach ($this->_freeBlocks as $loc=>$size) {
- if ($size >= $reqsize) {
- return $loc;
- }
- }
- }
- // no blocks available
- return false;
- }
- // }}}
-
- // {{{ _freeUsedBlock($key)
- /**
- * Places a used block on the free list, updates indicies accordingly
- *
- * @access private
- * @param string $key
- * @returns mixed
- */
- function _freeUsedBlock($key)
- {
- $loc = $this->_usedBlocks[$key][DBA_SIMPLE_LOC];
- $size = $this->_usedBlocks[$key][DBA_SIMPLE_SIZE];
- unset($this->_usedBlocks[$key]);
-
- $this->_freeBlocks[$loc] = $size;
- $this->_writeIdxEntry($loc, $size);
- }
- // }}}
-
- // {{{ create($dbName)
- /**
- * Creates a new database file if one does not exist. If it already exists,
- * updates the last-updated timestamp on the database
- *
- * @access public
- * @param string $dbName the database to create
- * @return object PEAR_Error on failure
- */
- function create($dbName)
- {
- if (!(@touch($dbName.'.dat') && @touch($dbName.'.idx'))) {
- return $this->raiseError('Could not create database: '.$dbName);
- }
- }
- // }}}
-
- // {{{ db_exists($dbName)
- /**
- * Indicates whether a database with given name exists
- *
- * @access public
- * @param string $dbName the database name to check for existence
- * @return boolean true if the database exists, false if it doesn't
- */
- function db_exists($dbName)
- {
- return(file_exists($dbName.'.dat') && file_exists($dbName.'.idx'));
- }
- // }}}
-
- // {{{ db_drop($dbName)
- /**
- * Removes a database from existence
- *
- * @access public
- * @param string $dbName the database name to drop
- * @return object PEAR_Error on failure
- */
- function db_drop($dbName)
- {
- if (DBA_Driver_File::db_exists($dbName)) {
- if (!unlink($dbName.'.dat') || !unlink($dbName.'.idx')) {
- return $this->raiseError(DBA_ERROR_CANNOT_DROP, NULL, NULL,
- 'dbname: '.$dbName);
- }
- } else {
- return $this->raiseError(DBA_ERROR_NOSUCHDB, NULL, NULL,
- 'dbname: '.$dbName);
- }
- }
- // }}}
-
- // {{{ drop($dbName)
- /**
- * Removes the last open database from existence
- *
- * @access public
- * @return object PEAR_Error on failure
- */
- function drop()
- {
- $this->close();
- return $this->db_drop($this->_dbName);
- }
- // }}}
-
- // {{{ exists($key)
- /**
- * Check whether key exists
- *
- * @access public
- * @param string $key
- * @return boolean true if the key exists, false if it doesn't
- */
- function exists($key)
- {
- return($this->isOpen() && isset($this->_usedBlocks[$key]));
- }
- // }}}
-
- // {{{ sync()
- /**
- * Synchronizes an open database to disk
- * @access public
- */
- function sync()
- {
- if ($this->isWritable()) {
- fflush($this->_datFP);
- fflush($this->_idxFP);
- }
- }
- // }}}
-
- // {{{ optimize()
- /**
- * Optimizes an open database
- * @access public
- */
- function optimize()
- {
- if ($this->isWritable()) {
- $this->_writeIdx();
- }
- }
- // }}}
-
- // {{{ _readIdx()
- /**
- * Reads the entries in an index file
- * Assumes that $this->_idxFP is valid and readable
- * @access private
- */
- function _readIdx()
- {
- // clear out old data if a previous database was opened
- $this->_usedBlocks = array();
- $this->_freeBlocks = array();
- $usedBlocks = array(); // temporary used index
- $key = ''; // reset key
-
- while (fscanf($this->_idxFP, '%u|%u|%u|%s', $loc, $size, $vsize, $key))
- {
- // is this an free block?
- if ($key == '') {
- // check if this block had been previously marked as used
- if (isset($usedBlocks[$loc])) {
- unset($this->_usedBlocks[$usedBlocks[$loc]]);
- unset($usedBlocks[$loc]);
- }
-
- $this->_freeBlocks[$loc] = $size;
- } else {
- // check if this block had been previously marked as free
- if (isset($this->_freeBlocks[$loc])) {
- unset($this->_freeBlocks[$loc]);
- }
-
- $this->_usedBlocks[$key] = array($loc, $size, $vsize);
- $usedBlocks[$loc] = $key;
- }
- $key = ''; // reset key for the next iteration
- }
- }
- // }}}
-
- // {{{ _writeIdx()
- /**
- * Rewrites the index file, removing free entries
- * Assumes that $this->_idxFP is valid and writable
- *
- * @access private
- */
- function _writeIdx()
- {
- // move the file pointer to the beginning; ftruncate does not do this
- fseek($this->_idxFP, 0);
-
- // clear the index
- ftruncate($this->_idxFP, 0);
-
- // write the free blocks
- if (isset($this->_freeBlocks)) {
- foreach ($this->_freeBlocks as $loc=>$size) {
- $this->_writeIdxEntry($loc,$size);
- }
- }
-
- // write the used blocks
- if (isset($this->_usedBlocks)) {
- foreach ($this->_usedBlocks as $key=>$block) {
- $this->_writeIdxEntry($block[DBA_SIMPLE_LOC],
- $block[DBA_SIMPLE_SIZE],
- $block[DBA_SIMPLE_VSIZE], $key);
- }
- }
- fflush($this->_idxFP);
- }
- // }}}
-
- // {{{ _writeIdxEntry($loc, $size, $vsize=NULL, $key=NULL)
- /**
- * Writes a used block entry to an index file
- *
- * @access private
- */
- function _writeIdxEntry($loc, $size, $vsize=NULL, $key=NULL)
- {
- if (is_null($vsize)) {
- // write a free block entry
- fputs($this->_idxFP, "$loc|$size\n");
- } else {
- // write a used block entry
- fputs($this->_idxFP, "$loc|$size|$vsize|$key\n");
- }
- }
- // }}}
- }
- ?>
-